package il.technion.cs236369.webserver;

import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
import java.util.concurrent.ArrayBlockingQueue;
import javax.net.ServerSocketFactory;

import org.apache.http.HttpResponse;
import org.apache.http.HttpVersion;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.DefaultHttpServerConnection;
import org.apache.http.impl.cookie.DateUtils;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.params.BasicHttpParams;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.name.Named;

public class WebServer {
	// The queues must be static in order for the threads to take things from them
	// (server object is sent to the threads through the queue) 
	private ArrayBlockingQueue<SocketWrapper> socketQueue;  //queue that holds sockets received from client
	private ArrayBlockingQueue<Request> requestQueue;                       //queue that holds requests

	private int listeningPort,numSocketReaders,numRequestHandlers,socketQueueSize,requestQueueSize;
	private String welcomeFile, baseDir;
	private ServerSocket serverListeningSocket;
	private ServerSocketFactory serverSocketFactory;
	public final ConfigParams supportedTypes;
	
	private SessionsHandler sessionsHandler;
	
	/**
	 * Constructs the WebServer
	 * 
	 * @param srvSockFactory
	 *            The ServerSocketFactory to be used for creating a single
	 *            ServerSocket for listening for clients requests
	 * 
	 * @param port
	 *            The port number to bounded by the ServerSocket
	 * 
	 * @param baseDir
	 *            The base directory of the server in the file system (absolute
	 *            path)
	 * @param welcomeFile
	 *            The file should be returned upon request of the base dir
	 * @param numSocketReaders
	 *            number of socket-reader threads
	 * @param numRequestHandlers
	 *            number of request-handler threads
	 * @param sizeSocketQueue
	 *            size of the socket queue (how many client sockets are saved)
	 * @param sizeRequestQueue
	 *            size of the socket queue (how many requests are saved)
	 */
	@Inject
	WebServer(ServerSocketFactory srvSockFactory,
			@Named("httpserver.net.port") int port,
			@Named("httpserver.app.baseDir") String baseDir,
			@Named("httpserver.app.welcomeFile") String welcomeFile,
			@Named("httpserver.threads.numSocketReaders") int numSocketReaders,
			@Named("httpserver.threads.numRequestHandlers") int numRequestHandlers,
			@Named("httpserver.queues.sizeSocketQueue") int sizeSocketQueue,
			@Named("httpserver.queues.sizeRequestQueue") int sizeRequestQueue) {

		this.serverSocketFactory = srvSockFactory;
		this.listeningPort = port;              
		this.welcomeFile = welcomeFile;
		this.baseDir = baseDir;
		this.numSocketReaders = numSocketReaders;
		this.numRequestHandlers = numRequestHandlers;
		this.socketQueueSize = sizeSocketQueue;
		this.requestQueueSize = sizeRequestQueue;

		//create sessions handle
		this.sessionsHandler = new SessionsHandler();
		
		//create waiting queues
		this.socketQueue = new ArrayBlockingQueue<SocketWrapper>(socketQueueSize);
		this.requestQueue = new ArrayBlockingQueue<Request>(requestQueueSize);

		//create threads (THESE LINES MUST BE CALLED AFTER CREATING THE QUEUES!)
		initiateSocketReaderThreads();
		initiateRequestHandlerThreads();
		
		File dummyFile = new File("");
		String path = dummyFile.getAbsolutePath();
		path = path+"\\config.xml";
		supportedTypes = new ConfigParams(path);
	}

	/**
	 * Create a new bounded server socket using ServerSocketFactory with the
	 * given port
	 * 
	 * @throws IOException
	 *             unable to bind the server socket
	 */
	public void bind() throws IOException {
		serverListeningSocket = serverSocketFactory.createServerSocket(listeningPort);
	}

	/**
	 * Starts the server loop: listens to client requests and executes them.
	 */
	public void start() {
		Socket socket = null;
		try {
			while(true) {
				try {
					//get the socket from client
					socket = serverListeningSocket.accept();
					//create socket wrapper and insert to the queue. if offer fails- return error 503 to user
					SocketWrapper socketWrapper = new SocketWrapper(socket,this);
					if(!socketQueue.offer(socketWrapper)){
						sendListenerErrorResponse(socket,503,"Service Unavailable",
								"Socket queue is full. Please wait and send request again",true);
					}
				} catch (Exception e) {
					e.printStackTrace();
					sendListenerErrorResponse(socket,500,"Internal Server Error","Listener error",true);
				}
			} 

		} finally { try{serverListeningSocket.close();} catch (IOException e) {e.printStackTrace();}
		}
	}

	/**
	 * Takes a socket from the queue. Blocks until there is something to take.
	 * @return - A socket wrapper that contains the socket
	 * @throws InterruptedException
	 */
	public SocketWrapper takeSocketFromQueue() throws InterruptedException {
		return socketQueue.take();
	}

	/**
	 * Puts a request in the queue. Blocks until there is enough space in queue.
	 * @param request
	 * @throws InterruptedException
	 */
	public void putRequestInQueue(Request request) throws InterruptedException {
		requestQueue.put(request);
	}

	/**
	 * Takes a request from the queue. Blocks until there is something to take.
	 * @return - A request object
	 * @throws InterruptedException
	 */
	public Request takeRequestFromQueue() {
		Request request = null;
		while (request == null) {
			try {
				request = requestQueue.take();
			} catch (InterruptedException ignore) { }
		}
		return request;
	}

	public SessionsHandler getSessionsHandler() {
		return sessionsHandler;
	}
	
	public String getBaseDir() {
		return baseDir;
	}

	public String getWelcomeFile() {
		return welcomeFile;
	}

	/**
	 * Sends an HTMl error response to the client, and enables closing the connection
	 * @param socket - the socket to send the response
	 * @param errorCode - the HTML code of the error
	 * @param errorStr - the HTTP message of the error as defined in {@link http://www.w3.org/Protocols/rfc2616/rfc2616.html}
	 * @param message - a message to put in the HTML page that is sent to the user
	 * @param closeConnection - true to close the connection conn after sending the error
	 */
	public void sendErrorResponse(DefaultHttpServerConnection conn,int errorCode, String errorStr,
			String message, boolean closeConnection) {
		try {
			HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1,errorCode,errorStr);
			// create the entity
			String entityStr = "<html> <head> <title>" + errorCode + " " + errorStr + 
					"</title> </head> <body> <h1>" + errorCode + " " + errorStr + 
					"</h1> <p>" + message +"</p> <hr /> </body> </html>";
			StringEntity entity = new StringEntity(entityStr);
			response.setEntity(entity);

			// add headers
			response.addHeader("Date", generateServerDate());
			response.addHeader("Content-Length", entity.getContentLength() + "");
			response.addHeader("Connection", "close");
			response.addHeader("Content-Type", "text/html");

			//send response
			conn.sendResponseHeader(response);
			conn.sendResponseEntity(response);

		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try{
				if(closeConnection) {
					conn.shutdown();
				}
			} catch(IOException e){
				e.printStackTrace();
			}
		}       
	}

	/**
	 * Returns the current date in this valid HTTP format:
	 * Sun, 06 Nov 1994 08:49:37 GMT     (RFC 822, updated by RFC 1123)
	 * @return
	 */
	public static String generateServerDate(){
		return DateUtils.formatDate(new Date(System.currentTimeMillis()), DateUtils.PATTERN_RFC1123);
	}

	/*
	 * Sends an HTML error from the listener
	 * More about the parameters: in function sendErrorResponse
	 */
	private void sendListenerErrorResponse(Socket socket,int errorCode, String errorStr,
			String message, boolean closeConnection){
		DefaultHttpServerConnection conn = new DefaultHttpServerConnection();
		try{
			BasicHttpParams params = new BasicHttpParams();
			conn.bind(socket, params);
			sendErrorResponse(conn,errorCode,errorStr,message,closeConnection);
		} catch (IOException e){
			e.printStackTrace();
		} 
	}

	/*
	 * start the running of the socket reader threads
	 */
	private void initiateSocketReaderThreads(){
		try{
			int i=0;
			for(i=0;i<numSocketReaders;i++){
				SocketReaderThread socketReaderThread = new SocketReaderThread();
				socketReaderThread.setWebServer(this);
				socketReaderThread.start();
			}
		} catch(Exception e) {
			e.printStackTrace();
		}
	}

	/*
	 * start the running of the request handler threads
	 */
	private void initiateRequestHandlerThreads(){
		try{
			int i=0;
			for(i=0;i<numRequestHandlers;i++){
				RequestHandlerThread requestHandlerThread = new RequestHandlerThread();
				requestHandlerThread.setWebServer(this);
				requestHandlerThread.start();
			}
		} catch(Exception e) {
			e.printStackTrace();
		}
	}

	public static void main(String[] args) throws Exception { 
		// assume that config.xml is in the project main path 
		Injector injector = Guice.createInjector(new WebServerModule()); 
		WebServer server = injector.getInstance(WebServer.class); 
		server.bind(); 
		server.start(); 
	}
}
